/**
 * ModGames (C) 2007 Pedro Miller Rabinovitch.
 * 
 * Permission is hereby granted to copy and use this material for any free or educational purpose.
 * Please do not use this for commercial purposes before letting me know (but it'll be fine).
 * 
 * Contact me via gmail, account pedromr
 * http://exodia.org
 * 
 * $Id: MODCanvas.java 25 2007-11-28 18:40:44Z pedromr $
 */
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.game.TiledLayer;

/**
 * The game canvas.
 * 
 * @author Miller
 */
public class MODCanvas extends GameCanvas implements Runnable {

	/**
	 * The following are configurable parameters:
	 */
	/**
	 * Screen background.
	 */
	private int paramRgbBack;

	/**
	 * Number of initial segments on the snake.
	 */
	private int paramStartingSegments;

	private String paramPathTileset;

	private String paramPathLevelMap;

	/**
	 * Graphics object for the game canvas.
	 */
	private Graphics g;

	/**
	 * The game is running.
	 */
	private boolean bRunning;

	/**
	 * The screen has been invalidated.
	 */
	private boolean bNeedsRepaint;

	/**
	 * The game is starting.
	 */
	public final static int S_STARTING = 0;

	/**
	 * The snake is in motion.
	 */
	public final static int S_PLAYING = 1;

	/**
	 * The snake is dead.
	 */
	public final static int S_DEAD = 2;

	/**
	 * The game is paused.
	 */
	public final static int S_PAUSED = 3;

	private static final int MAX_TILES_PER_ANIM = 31;

	/**
	 * Game state (S_*).
	 */
	private int state;

	/**
	 * Number of current segments on the snake.
	 */
	private int nSegments;

	/**
	 * Number of segments the snake has left to grow (just ate or game just
	 * began).
	 */
	private int nSegmentsToGrow;

	/**
	 * Level tilemap.
	 */
	private TiledLayer tilemap;

	/**
	 * Tileset image.
	 */
	private Image imgTileset;

	private int paramTileWidth;

	private int paramTileHeight;

	private String paramPathHead;

	private String paramPathBody;

	private String paramPathTail;

	private int paramFramesHead;

	private int paramFramesTail;

	private int paramFramesBody;

	private SnakePiece snakeTail;

	private SnakePiece snakeBodyPieces[];

	private int paramMaxSnakeSize;

	private long timeSinceLastMove;

	private long timeForMove;

	private int foodX, foodY;

	private int timeUntilNextState;

	private SnakePiece snakeHead;

	private int mapWidth;

	private int mapHeight;

	private int [][]paramTileAnimations;

	private int []currentTileAnimationFrames;
	
	private boolean []tileCollisionList;

	public MODCanvas() {
		super(true);
		paramRgbBack = 0x000000;
		paramPathTileset = "/tiles.png";
		paramPathLevelMap = "/level1.txt";
		paramStartingSegments = 5;
		paramTileHeight = 16;
		paramTileWidth = 16;
		paramPathHead = "/shead.png";
		paramPathBody = "/sbody.png";
		paramPathTail = "/stail.png";
		paramMaxSnakeSize = 30;

		g = getGraphics();
	}

	/**
	 * Starts the main thread.
	 */
	public void runGame() {

		long start = System.currentTimeMillis();
		readConfiguration();
		long delta = System.currentTimeMillis() - start;

		log("+ readConfiguration() took " + delta + " ms.");

		initLevel();

		new Thread(this).start();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		bRunning = true;
		long lastTick = System.currentTimeMillis();

		try {

			while (bRunning) {
				long now = System.currentTimeMillis();
				long deltaMS = now - lastTick;

				if (deltaMS > 0 && deltaMS < 300) 
				{
					// otherwise we skip the frame
					tick(deltaMS);
				}

				if (bNeedsRepaint)
					paint();				

				serviceRepaints();
				Thread.yield();
					
				lastTick = now;
			}
		} catch (Throwable e) {
			log("Exception in run(): ");
			e.printStackTrace();
		}
	}

	/**
	 * Updates a frame.
	 * 
	 * @param deltaMS
	 *            number of milliseconds since last frame update
	 */
	private void tick(long deltaMS) {

		switch (state) {
		case S_PLAYING:
			timeSinceLastMove += deltaMS;
			while (timeSinceLastMove > timeForMove) {
				timeSinceLastMove -= timeForMove;
				moveSnake();
				bNeedsRepaint = true;
			}
			break;
		case S_DEAD:
			timeUntilNextState -= deltaMS;
			if (timeUntilNextState <= 0)
				respawnSnake();
			break;
		}
	}

	/**
	 * Moves the snake a step forward.
	 */
	private void moveSnake() {
		SnakePiece piece, neck = snakeTail;

		if (nSegmentsToGrow > 0) {
			// grow a body piece
			piece = snakeBodyPieces[nSegments - 2];
			nSegmentsToGrow--;
			nSegments++;

			piece.ahead = snakeTail.ahead;
			piece.stepForward();
			updatePiecePosition(piece);
			snakeTail.ahead = piece;
		}

		// check key state and set head orientation
		int keyState = getKeyStates();

		if (snakeHead.orientation != SnakePiece.O_UP
				&& (keyState & DOWN_PRESSED) != 0) {
			snakeHead.setOrientation(SnakePiece.O_DOWN);
		} else if (snakeHead.orientation != SnakePiece.O_DOWN
				&& (keyState & UP_PRESSED) != 0) {
			snakeHead.setOrientation(SnakePiece.O_UP);
		} else if (snakeHead.orientation != SnakePiece.O_LEFT
				&& (keyState & RIGHT_PRESSED) != 0) {
			snakeHead.setOrientation(SnakePiece.O_RIGHT);
		} else if (snakeHead.orientation != SnakePiece.O_RIGHT
				&& (keyState & LEFT_PRESSED) != 0) {
			snakeHead.setOrientation(SnakePiece.O_LEFT);
		}

		// traverse body copying the piece ahead
		// (in head's case, move forward)
		for (piece = snakeTail; piece != null; piece = piece.ahead) {
			piece.stepForward();
			updatePiecePosition(piece);
			if (piece.ahead == snakeHead)
				neck = piece;
		}

		// we need to adjust neck orientation. The rest will follow its lead.
		neck.setOrientation(snakeHead.orientation);
		updatePiecePosition(neck);

		snakeHead.col = (snakeHead.col + mapWidth) % mapWidth;
		snakeHead.row = (snakeHead.row + mapHeight) % mapHeight;
		updatePiecePosition(snakeHead);

		int tile = tilemap.getCell(snakeHead.col, snakeHead.row);
		if (isCollidable(tile)) {
			die();
		}
		for (piece = snakeTail; piece.ahead != null; piece = piece.ahead) {
			if (snakeHead.col == piece.col && snakeHead.row == piece.row)
				die();
		}
		
		animateTiles();
	}

	private void animateTiles() {
		if( paramTileAnimations != null ) {		
			for( int i = 0; i < paramTileAnimations.length; i++ ) {
				int f = currentTileAnimationFrames[i]+1;
				if( paramTileAnimations[i][f] == -1 )
					f = 0;
				currentTileAnimationFrames[i] = f;
				tilemap.setAnimatedTile(-i-1, paramTileAnimations[i][f]);
			}
		}
	}

	private void die() {
		state = S_DEAD;
		snakeHead.setVisible(false);
		timeUntilNextState = 1500;
		bNeedsRepaint = true;
	}

	private boolean isCollidable(int tile) {
		//return (tile == 1);
		
		return tileCollisionList[tile + nAnimatedTiles];
	}

	private void updatePiecePosition(SnakePiece piece) {
		final int tilemapX = tilemap.getX();
		final int tilemapY = tilemap.getY();

		int x = tilemapX + piece.col * paramTileWidth;
		int y = tilemapY + piece.row * paramTileHeight;
		piece.setPosition(x, y);
	}

	/**
	 * Repaints the screen.
	 */
	public void paint() {
		if( g == null )
			g = getGraphics();
		g.setColor(paramRgbBack);
		g.fillRect(0, 0, getWidth(), getHeight());

		if (tilemap != null)
			tilemap.paint(g);

		if (snakeTail != null) {
			SnakePiece piece;
			for (piece = snakeTail; piece != null; piece = piece.ahead) {
				piece.paint(g);
			}
		}

		repaint();
	}

	/**
	 * Starts a level.
	 */
	public void initLevel() {
		state = S_STARTING;
		timeForMove = paramInitialSpeed;

		loadLevelMap();
		loadGameResources();

		respawnSnake();
	}

	private void readConfiguration() {
		propertyReader = new TextConfigReader();
		Hashtable properties = propertyReader.properties;

		properties.put("game.snakeSpeed", "250");
		properties.put("snake.maxSize", "20");
		properties.put("snake.head", "shead.png");
		properties.put("snake.body", "sbody.png");
		properties.put("snake.tail", "stail.png");
		properties.put("game.snakeStart", "4");
		properties.put("tiles.image", "tiles.png,16,16");
		properties.put("tiles.collision", "1");

		propertyReader.readFile("/art.txt");
		propertyReader.readFile("/game.txt");

		// System.out.println("Properties:");
		// Enumeration keys = properties.keys();
		// while (keys.hasMoreElements()) {
		// String key = (String) keys.nextElement();
		// System.out.println(key + ":" + properties.get(key));
		// }

		paramStartingSegments = propertyReader.getInt("game.snakeStart");
		paramInitialSpeed = propertyReader.getInt("game.snakeSpeed");
		paramMaxSnakeSize = propertyReader.getInt("snake.maxSize");

		paramPathHead = propertyReader.getFromSequence("snake.head");
		paramFramesHead = propertyReader.getIntFromSequence(null);
		paramPathTail = propertyReader.getFromSequence("snake.tail");
		paramFramesTail = propertyReader.getIntFromSequence(null);
		paramPathBody = propertyReader.getFromSequence("snake.body");
		paramFramesBody = propertyReader.getIntFromSequence(null);

		paramPathTileset = propertyReader.getFromSequence("tiles.image");
		paramTileWidth = propertyReader.getIntFromSequence(null);
		paramTileHeight = propertyReader.getIntFromSequence(null);
		
		String name = "tiles.anim.";
		nAnimatedTiles = 0;
		while( propertyReader.prepareSequence(name+(nAnimatedTiles+1))) {
			nAnimatedTiles++;
		}
		
		if( nAnimatedTiles > 0 ) {
			paramTileAnimations = new int[nAnimatedTiles][MAX_TILES_PER_ANIM+1];
			currentTileAnimationFrames = new int[nAnimatedTiles];
			
			for( int i=0; i < nAnimatedTiles; i++ ) {
				int j = 0;
				propertyReader.prepareSequence(name+(i+1));
				while(propertyReader.sequenceHasAvailable()) {
					paramTileAnimations[i][j] = propertyReader.getIntFromSequence(null);
					j++;
				}
				paramTileAnimations[i][j] = -1; // end of anim marker
				
				currentTileAnimationFrames[i] = 0;
			}
		}
		
	}

	private String fixResPath(String path) {
		return (path.charAt(0) == '/') ? path : "/" + path;
	}

	public void respawnSnake() {
		nSegments = 2;
		nSegmentsToGrow = paramStartingSegments - nSegments;

		timeSinceLastMove = 0;
		snakeHead.col = paramStartingX;
		snakeHead.row = paramStartingY;
		snakeHead.setOrientation(paramStartingOrientation);
		snakeTail.ahead = snakeHead;
		snakeTail.stepForward();

		updatePiecePosition(snakeTail);
		updatePiecePosition(snakeHead);
		snakeHead.setVisible(true);

		state = S_PLAYING;
	}

	private void loadGameResources() {
		SnakePiece head = new SnakePiece(loadSprite(paramPathHead,
				paramFramesHead), null);
		SnakePiece tail = new SnakePiece(loadSprite(paramPathTail,
				paramFramesTail), head);

		Sprite spriteBody = loadSprite(paramPathBody, paramFramesBody);

		snakeBodyPieces = new SnakePiece[paramMaxSnakeSize - 2];
		for (int i = 0; i < snakeBodyPieces.length; i++) {
			snakeBodyPieces[i] = new SnakePiece(spriteBody, null);
		}

		head.col = head.row = 3;
		tail.stepForward();

		snakeTail = tail;
		snakeHead = head;
	}

	/**
	 * Load a sprite from a horizontal strip.
	 * 
	 * @param path
	 *            Path to the image file
	 * @param numberOfFrames
	 *            Number of frames in the sprite
	 * @return
	 */
	private Sprite loadSprite(String path, int numberOfFrames) {
		int frameWidth, frameHeight;

		Image img = loadImage(path);

		frameWidth = img.getWidth() / numberOfFrames;
		frameHeight = img.getHeight();

		log(". creating sprite from " + path + " with " + numberOfFrames
				+ " frames (" + frameWidth + "x" + frameHeight + ")");

		return new Sprite(img, frameWidth, frameHeight);
	}

	/**
	 * Last integer read by readInteger.
	 */
	private int intRead;

	private int paramInitialSpeed;

	private int paramStartingX;

	private int paramStartingY;

	private int paramStartingOrientation;

	private int nAnimatedTiles;

	private TextConfigReader propertyReader;

	/**
	 * Reads the next integer in the stream.
	 * 
	 * @param in
	 *            Input stream
	 * @param first
	 *            First character to be considered, or -1 if none were read so
	 *            far
	 * @return the next character read, and intRead is set to the value
	 * @throws IOException
	 */
	private int readInteger(InputStream in, int first) throws IOException {
		int num = 0;

		if (first == -1)
			first = in.read();

		while (first == ' ' || first == '\t' || first == '\n' || first == '\r')
			first = in.read();

		if (first == -1) {
			intRead = Integer.MAX_VALUE;
			return first; // stream is over
		}

		boolean neg = false;
		if (first == '-') {
			neg = true;
			first = in.read();
		}

		while (first >= '0' && first <= '9') {
			num *= 10;
			num += first - '0';

			first = in.read();
		}

		intRead = (neg) ? -num : num;

		return first;
	}

	/**
	 * Loads the level map.
	 */
	private void loadLevelMap() {
		if (imgTileset == null) {
			imgTileset = loadImage(paramPathTileset);
		}

		InputStream in = this.getClass()
				.getResourceAsStream(fixResPath(paramPathLevelMap));
		int first = -1;
		try {
			first = readInteger(in, first);
			mapWidth = intRead;
			first = readInteger(in, first);
			mapHeight = intRead;

			tilemap = new TiledLayer(mapWidth, mapHeight, imgTileset,
					paramTileWidth, paramTileHeight);

			int nTotalTiles = ( imgTileset.getWidth()/paramTileWidth ) * ( imgTileset.getHeight()/paramTileHeight );
				
			if( paramTileAnimations != null ) {				
				for( int i=0; i < nAnimatedTiles; i++ ) {
					tilemap.createAnimatedTile(paramTileAnimations[i][0]);
				}
				nTotalTiles += nAnimatedTiles;
			}
			
			
			tileCollisionList = new boolean[ nTotalTiles ];
			for( int i=0; i < nTotalTiles; i++ ) {
				tileCollisionList[i] = false;
			}
			propertyReader.prepareSequence("tiles.collision");
			while( propertyReader.sequenceHasAvailable() ) {
				tileCollisionList[ propertyReader.getIntFromSequence(null) + nAnimatedTiles ] = true;
			}
			                			
			int direction = 0;
			for (int i = 0; i < mapHeight; i++) {
				for (int j = 0; j < mapWidth; j++) {
					first = readInteger(in, first);
					//log( "setting tile "+j+","+i+","+intRead);
					tilemap.setCell(j, i, intRead);
					if (direction == 0) {
						// check for starting position + direction
						switch (first) {
						case '>':
							direction = SnakePiece.O_RIGHT;
							break;
						case '<':
							direction = SnakePiece.O_LEFT;
							break;
						case '^':
							direction = SnakePiece.O_UP;
							break;
						case 'v':
							direction = SnakePiece.O_DOWN;
							break;
						default:
							break;
						}
						if (direction != 0) {
							paramStartingX = j;
							paramStartingY = i;
							paramStartingOrientation = direction;
							first = in.read();
						}
					}
				}
			}

			int x = getWidth() / 2 - tilemap.getWidth() / 2;
			int y = getHeight() / 2 - tilemap.getHeight() / 2;
			tilemap.setPosition(x, y);

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void log(String string) {
		System.out.println(string);
	}

	private Image loadImage(String path) {
		try {
			path = fixResPath(path);
			// log(". loading image " + path);
			return Image.createImage(path);
		} catch (IOException e) {
			e.printStackTrace();
		}

		return null;
	}
}
